In previous articles, I explained how we could use C# and MongoDB to get records inserted. But we started this exploration into MongoDB covering a lot of the basics and I wanted to start looking into more interesting aspects of how we use these things together. As I was putting video content together about all of these topics, one thing that caught my attention was the generic methods vs methods that operated on BsonDocument and I was curious if the performance was any different. So to start, I figured we'd look at C# MongoDB insert benchmarks and see if any interesting patterns stand out.
Remember to check out these platforms:
Considerations For These C# MongoDB Benchmarks
I was chatting with David Callan on Twitter the other day about benchmarks that he often posts on social media. Turns out there was another discussion floating around the Twitter-verse where it was being debated about sync vs async calls to databases. The proposal was that for very fast DB queries, the async variations are indeed slower.
Of course, this got the gears turning. I have been writing about and creating videos for MongoDB in C# all week and hinting at performance benchmarks coming. This coupled with my conversation with Dave AND the conversation I saw on Twitter made me think that I had to give this more attention. I speculated it might have something to do with Task vs ValueTask, but I didn't have a lot of grounds for that.
However, I originally wanted to look into these benchmarks because I was curious if using BsonDocument compared to a dedicated DTO (whether it was a struct, class, or record variation) would have different performance characteristics.
That meant that I would want to ensure I covered a matrix across:
- Synchronous
- Asynchronous
- Asynchronous with ValueTask
- Struct
- Class
- Record Struct
- Record Class
- BsonDocument
And with that, I set off to go write some super simple benchmarks! Let's check them out.
The C# MongoDB Insert Benchmarking Code
As with all benchmarking that we do in C#, BenchmarkDotNet is our go-to tool! Make sure you start by getting the BenchmarkDotNet NuGet package installed. We'll be using this to ensure we have consistent setup, warmup, runs, and reporting for our MongoDB benchmarks.
Since we're trying to reduce as many possible external factors as we can for these benchmarks, we're going to be using Testcontainers to run an instance of MongoDB in a Docker container. Of course, by interacting with anything outside of our code directly there are opportunities for inconsistencies and more room for error to show up in our results. However, this should help minimize things. You'll want to get the Testcontainers.MongoDB NuGet package for this as well.
You can find all the relevant code on GitHub, but we'll start with what our entry point looks like:
Historical baseline: The linked project manifest targets net8.0 with MongoDB.Driver 2.24.0, BenchmarkDotNet 0.13.12, and Testcontainers.MongoDb 3.8.0. The setup below uses the mutable mongo:latest tag; the exact server version or image digest used for the recorded measurements is not established here. These are the original results, not a present-day rerun; a reproducible new experiment would need its own recorded image identity and measurements.
using BenchmarkDotNet.Running;
using System.Reflection;
BenchmarkRunner.Run(
Assembly.GetExecutingAssembly(),
args: args);
Nice and simple just to kick off the benchmarks. And the benchmarks are the most important part here:
using BenchmarkDotNet.Attributes;
using MongoDB.Bson;
using MongoDB.Driver;
using Testcontainers.MongoDb;
[MemoryDiagnoser]
//[ShortRunJob]
[MediumRunJob]
public class InsertBenchmarks
{
private MongoDbContainer? _container;
private MongoClient? _mongoClient;
private IMongoCollection<BsonDocument>? _collection;
private IMongoCollection<RecordStructDto>? _collectionRecordStruct;
private IMongoCollection<RecordClassDto>? _collectionRecordClass;
private IMongoCollection<StructDto>? _collectionStruct;
private IMongoCollection<ClassDto>? _collectionClass;
[GlobalSetup]
public async Task SetupAsync()
{
_container = new MongoDbBuilder()
.WithImage("mongo:latest")
.Build();
await _container.StartAsync();
_mongoClient = new MongoClient(_container.GetConnectionString());
var database = _mongoClient.GetDatabase("test");
_collection = database.GetCollection<BsonDocument>("test");
_collectionRecordStruct = database.GetCollection<RecordStructDto>("test");
_collectionRecordClass = database.GetCollection<RecordClassDto>("test");
_collectionStruct = database.GetCollection<StructDto>("test");
_collectionClass = database.GetCollection<ClassDto>("test");
}
[GlobalCleanup]
public async Task CleanupAsync()
{
await _container!.StopAsync();
}
[Benchmark]
public async Task InsertOneAsync_BsonDocument()
{
await _collection!.InsertOneAsync(new BsonDocument()
{
["Name"] = "Nick Cosentino",
});
}
[Benchmark]
public async ValueTask InsertOneAsyncValueTask_BsonDocument()
{
await _collection!.InsertOneAsync(new BsonDocument()
{
["Name"] = "Nick Cosentino",
});
}
[Benchmark]
public void InsertOne_BsonDocument()
{
_collection!.InsertOne(new BsonDocument()
{
["Name"] = "Nick Cosentino",
});
}
[Benchmark]
public async Task InsertOneAsync_RecordStruct()
{
await _collectionRecordStruct!.InsertOneAsync(new RecordStructDto("Nick Cosentino"));
}
[Benchmark]
public async ValueTask InsertOneAsyncValueTask_RecordStruct()
{
await _collectionRecordStruct!.InsertOneAsync(new RecordStructDto("Nick Cosentino"));
}
[Benchmark]
public void InsertOne_RecordStruct()
{
_collectionRecordStruct!.InsertOne(new RecordStructDto("Nick Cosentino"));
}
[Benchmark]
public async Task InsertOneAsync_RecordClass()
{
await _collectionRecordClass!.InsertOneAsync(new RecordClassDto("Nick Cosentino"));
}
[Benchmark]
public async ValueTask InsertOneAsyncValueTask_RecordClass()
{
await _collectionRecordClass!.InsertOneAsync(new RecordClassDto("Nick Cosentino"));
}
[Benchmark]
public void InsertOne_RecordClass()
{
_collectionRecordClass!.InsertOne(new RecordClassDto("Nick Cosentino"));
}
[Benchmark]
public async Task InsertOneAsync_Struct()
{
await _collectionStruct!.InsertOneAsync(new StructDto() { Name = "Nick Cosentino" });
}
[Benchmark]
public async ValueTask InsertOneAsyncValueTask_Struct()
{
await _collectionStruct!.InsertOneAsync(new StructDto() { Name = "Nick Cosentino" });
}
[Benchmark]
public void InsertOne_Struct()
{
_collectionStruct!.InsertOne(new StructDto() { Name = "Nick Cosentino" });
}
[Benchmark]
public async Task InsertOneAsync_Class()
{
await _collectionClass!.InsertOneAsync(new ClassDto() { Name = "Nick Cosentino" });
}
[Benchmark]
public async ValueTask InsertOneAsyncValueTask_Class()
{
await _collectionClass!.InsertOneAsync(new ClassDto() { Name = "Nick Cosentino" });
}
[Benchmark]
public void InsertOne_Class()
{
_collectionClass!.InsertOne(new ClassDto() { Name = "Nick Cosentino" });
}
private record struct RecordStructDto(string Name);
private record class RecordClassDto(string Name);
private struct StructDto
{
public string Name { get; set; }
}
private class ClassDto
{
public string Name { get; set; }
}
}
The benchmark code has as much as possible that we're not interested in exercising pulled out into the GlobalSetup and GlobalCleanup marked methods. The ValueTask-labelled methods are wrappers that await the same InsertOneAsync calls as the Task-returning methods. They compare wrapper return types, not a separate MongoDB ValueTask insertion API.
Results For C# MongoDB Insert Benchmarks
Cue the drumroll! It's time to look at our MongoDB benchmark results and do a little bit of an analysis:

Here are my takeaways from the benchmark data above:
- All async variations used ~5KB more than the normal versions of the methods that we had to use.
- There didn't seem to be any difference in allocated memory for async vs async value task BUT Gen0 and Gen1 didn't have any value for *some* of the ValueTask benchmarks -- However, not for all of them. It almost looks like ValueTask combined with a struct data type for the insert results in Gen0 and Gen1 with no value, but plain old BsonDocument is an exception to this.
- The fastest and lowest memory footprint seems to be InsertOne_RecordClass, although the InsertOne_BsonDocument is only a few microseconds off from this.
- Async versions of the benchmarks seem slower than their normal versions across the board as well
These observations apply to the recorded single-document insertion workload and its measured latency and allocations. They do not establish concurrent request throughput, application responsiveness, or a universal rule for database APIs. With that scope in mind:
- The original discussion reports higher latency for async variants in this experiment. That observation does not establish that async is generally worse for fast database operations; the historical timings have not been independently remeasured in this update.
- The original discussion did not identify a consistent ValueTask-wrapper advantage in this experiment. These methods wrap the same InsertOneAsync call; no new benchmark measurements were made for this update.
- For single items, there's no big difference in the memory footprint we're seeing between any of these variations of the data types
It'll be a good exercise to follow up with benchmarking inserting many items into MongoDB from C#. I think we may start to see some of these variations stand out in different ways once we're working with collections of items -- But this is still a hypothesis that needs to be proven!
You can watch my walkthrough of these benchmarks in this video:
Wrapping Up C# MongoDB Insert Benchmarks
This was a simple investigation of single-document insertion latency and allocations for MongoDB using C#. Its results are specific to the recorded setup, not proof that synchronous calls are the better choice for every application. Awaiting an incomplete operation does not block the thread, so API selection also depends on responsiveness and concurrent workload requirements that this benchmark did not measure.
If you found this useful and you're looking for more learning opportunities, consider subscribing to my free weekly software engineering newsletter and check out my free videos on YouTube! Meet other like-minded software engineers and join my Discord community!
Affiliations
These are products & services that I trust, use, and love. I get a kickback if you decide to use my links. There’s no pressure, but I only promote things that I like to use!
- BrandGhost: My social media content and scheduling tool that I use for ALL of my content!
- RackNerd: Cheap VPS hosting options that I love for low-resource usage!
- Contabo: Affordable VPS hosting options!
- ConvertKit: The platform I use for my newsletter!
- SparkLoop: Helps add value to my newsletter!
- Opus Clip: Tool for creating short-form videos!
- Newegg: For all sorts of computer components!
- Bulk Supplements: Huge selection of health supplements!
- Quora: I answer questions when folks request them!
Frequently Asked Questions: C# MongoDB Insert Benchmarks
How can I benchmark MongoDB from C#?
You can benchmark C# interactions with MongoDB by using BenchmarkDotNet. To have a more repeatable environment, you can use Testcontainers to assist by using Docker containers for MongoDB.
Is InsertOneAsync faster than InsertOne using MongoDB C# driver?
The original article reports higher InsertOneAsync latency in its historical single-document experiment. This update has not independently remeasured those timings. That report does not determine application throughput or responsiveness; those require workload-specific measurements.
How can I connect to MongoDB from C#?
You can use the MongoDB.Driver NuGet package that's available. If you don't want to host your database separately, you can use Testcontainers in your development or rest environment to work with MongoDB from C#.

